package br.ufpe.cin.imlm2.util.options;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Defines a named option (argument) which may have an alias (usually short
 * version of the name), a description, a mandatory default value and some
 * constraints on which values it may assume. The constrains may be a list of
 * possible values, a lower bound and/or an upper bound. If lower and/or upper
 * bounds are defined then the type of this options value must be an instance of
 * <code>java.lang.Comparable</code>. Possible type values are
 * String,Integer,Long,Float,Double, Boolean and Lists of the pevious types
 * 
 * @author irineu
 * 
 */
public class Option {

	private String name;
	private String alias;
	private String description;
	private Class<?> type;
	private Class<?> componentType;
	private final Object defaultValue;
	private Object value;
	private List<Object> allowedValues;
	private Object upperBound;
	private Object lowerBound;

	private static final List<Class<?>> VALID_COMPONENT_TYPES = new ArrayList<Class<?>>();

	static {
		VALID_COMPONENT_TYPES.add(String.class);
		VALID_COMPONENT_TYPES.add(Integer.class);
		VALID_COMPONENT_TYPES.add(Long.class);
		VALID_COMPONENT_TYPES.add(Float.class);
		VALID_COMPONENT_TYPES.add(Double.class);
		VALID_COMPONENT_TYPES.add(Boolean.class);
	}

	public static class OptionBuilder {

		private String name;
		private String alias;
		private String description;
		private Class<?> type;
		private Class<?> componenType;
		private Object defaultValue;
		private List<Object> allowedValues;
		private Object upperBound;
		private Object lowerBound;

		private static final List<Class<?>> VALID_COMPARABLE_TYPES = new ArrayList<Class<?>>();

		static {
			VALID_COMPARABLE_TYPES.add(String.class);
			VALID_COMPARABLE_TYPES.add(Integer.class);
			VALID_COMPARABLE_TYPES.add(Long.class);
			VALID_COMPARABLE_TYPES.add(Float.class);
			VALID_COMPARABLE_TYPES.add(Double.class);
			VALID_COMPARABLE_TYPES.add(Boolean.class);
		}

		public OptionBuilder(String name, Object defaultValue, Class<?> type,
				Class<?> componentType) {
			if (name != null) {
				this.name = name;
				this.alias = name;
			} else
				throw new IllegalArgumentException(
						"An option must have a name.");
			if (defaultValue != null) {
				this.defaultValue = defaultValue;
			}
			this.type = type;
			if (type.equals(List.class)) {
				if (VALID_COMPONENT_TYPES.contains(componentType))
					this.componenType = componentType;
				else
					throw new IllegalArgumentException(
							"Invalid list component type for option "
									+ this.name);
			}
		}

		public OptionBuilder alias(String alias) {
			if (alias != null)
				this.alias = alias;
			return this;
		}

		public OptionBuilder description(String desc) {
			this.description = desc;
			return this;
		}

		public OptionBuilder allowedValues(List<Object> values) {
			if (!this.type.equals(List.class)) {
				if (values.contains(this.defaultValue))
					this.allowedValues = values;
				else
					throw new IllegalArgumentException(
							"Allowed values list doesn't contain default value.");
			} else {
				if (values.containsAll((Collection<?>) this.defaultValue))
					this.allowedValues = values;
				else
					throw new IllegalArgumentException(
							"Some default values are not in the allowed values list.");
			}

			return this;
		}

		@SuppressWarnings("unchecked")
		public OptionBuilder upperBound(Object upper) {
			if (VALID_COMPARABLE_TYPES.contains(this.type))
				if (((Comparable<Object>) upper).compareTo(this.defaultValue) >= 0)
					if (lowerBound == null
							|| (((Comparable<Object>) upper)
									.compareTo(lowerBound) >= 0))
						this.upperBound = upper;
					else
						throw new IllegalArgumentException(
								"Upper bounds can't be bellow lower bounds.");
				else
					throw new IllegalArgumentException(
							"Default value above upper bound.");
			else
				throw new IllegalArgumentException(
						"Type must implement Comparable<T> interface to set upper and lower bounds");
			return this;
		}

		@SuppressWarnings("unchecked")
		public OptionBuilder lowerBound(Object lower) {
			if (VALID_COMPARABLE_TYPES.contains(this.type))
				if (((Comparable<Object>) lower).compareTo(this.defaultValue) <= 0)
					if (upperBound == null
							|| (((Comparable<Object>) upperBound)
									.compareTo(lower) >= 0))
						this.lowerBound = lower;
					else
						throw new IllegalArgumentException(
								"Lower bound can't be above upper bound.");
				else
					throw new IllegalArgumentException(
							"Default value below lower bound.");
			else
				throw new IllegalArgumentException(
						"Type must implement Comparable<T> interface to set upper and lower bounds");
			return this;
		}

		public Option build() {
			return new Option(name, alias, description, type, componenType,
					defaultValue, allowedValues, upperBound, lowerBound);
		}

	}

	private Option(String name, String alias, String description,
			Class<?> type, Class<?> componenType, Object defaultValue,
			List<Object> allowedValues, Object upperBound, Object lowerBound) {
		super();
		this.name = name;
		this.alias = alias;
		this.description = description;
		this.type = type;
		this.componentType = componenType;
		this.defaultValue = defaultValue;
		this.value = defaultValue;
		this.allowedValues = allowedValues;
		this.upperBound = upperBound;
		this.lowerBound = lowerBound;
	}

	public Option(String name, String alias, String description,
			Object defaultVal, Class<?> type, Class<?> componentType) {
		super();
		this.verifyConstructorConstraints(name, alias, description, defaultVal,
				type, componentType);
		this.name = name;
		if (alias != null)
			this.alias = alias;
		else
			this.alias = this.name;
		this.description = description;
		this.defaultValue = defaultVal;
		this.value = defaultVal;
		this.type = type;
		if (this.type.equals(List.class)) {
			if (VALID_COMPONENT_TYPES.contains(componentType))
				this.componentType = componentType;
			else
				throw new IllegalArgumentException(
						"Invalid list component type for option " + this.name);
		}

	}

	private void verifyConstructorConstraints(String name2, String alias2,
			String description2, Object defaultVal, Class<?> type2,
			Class<?> componentType2) {
		if (name2 == null)
			throw new IllegalArgumentException("An option must have a name.");
		/*
		 * if(defaultVal == null) throw new
		 * IllegalArgumentException("Every option must have a default value");
		 */
		if (type2 == null)
			throw new IllegalArgumentException(
					"A type must be defined for an option.");
		else if (defaultVal != null && !type2.isInstance(defaultVal))
			throw new IllegalArgumentException("Default value is of type "
					+ defaultVal.getClass().toString()
					+ " which is NOT assignment compatible "
					+ "with a variable of type " + type2.toString());

	}

	public Option(String name, String alias, String description,
			Object defaultVal, Class<?> componentType) {
		this(name, alias, description, defaultVal, List.class, componentType);
	}

	public Option(String name, String alias, String description,
			Object defaultVal) {
		this(name, alias, description, defaultVal, defaultVal.getClass(), null);
	}

	public String getName() {
		return this.name;
	}

	public String getAlias() {
		return this.alias;
	}

	public void setAlias(String alias) {
		this.alias = alias;
	}

	public String getDescription() {
		return this.description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public Object getValue() {
		return this.value;
	}

	public void setValue(Object value) throws InvalidValueException {
		verifyConstraints(value);
		this.value = value;
	}

	@SuppressWarnings("unchecked")
	private void verifyConstraints(Object value) throws InvalidValueException {
		if (!this.type.isInstance(value))
			throw new InvalidValueException("Invalid value type.", value);
		if (this.allowedValues != null) {
			if (!this.type.equals(List.class)
					&& !this.allowedValues.contains(value))
				throw new InvalidValueException("Value " + value.toString()
						+ "  not in allowed values list.", value);
			else if (this.type.equals(List.class)
					&& !this.allowedValues.containsAll((Collection<?>) value))
				throw new InvalidValueException("Some values in list "
						+ value.toString()
						+ " are not in the allowed values list "
						+ this.allowedValues.toString(), value);

		}
		if (this.upperBound != null && this.lowerBound != null) {
			if (value instanceof Comparable<?>) {
				Comparable<Object> upper = (Comparable<Object>) this.upperBound;
				Comparable<Object> lower = (Comparable<Object>) this.lowerBound;
				if (upper.compareTo(value) < 0 || lower.compareTo(value) > 0) {
					throw new InvalidValueException(
							"Value outside of bounds [" + lower.toString()
									+ " - " + upper.toString() + "]", value);
				}
			} else
				throw new InvalidValueException(
						"Value must implement Comparable<T> interface.", value);
		} else if (this.upperBound != null) {
			if (value instanceof Comparable<?>) {
				Comparable<Object> upper = (Comparable<Object>) this.upperBound;
				if (upper.compareTo(value) < 0)
					throw new InvalidValueException(
							"Value above upper bound [... - "
									+ upper.toString() + "]", value);
			} else
				throw new InvalidValueException(
						"Value must implement Comparable<T> interface.", value);
		} else if (this.lowerBound != null) {
			if (value instanceof Comparable<?>) {
				Comparable<Object> lower = (Comparable<Object>) this.lowerBound;
				if (lower.compareTo(value) > 0)
					throw new InvalidValueException("Value below lower bound ["
							+ lower.toString() + " - ...]", value);
			} else
				throw new InvalidValueException(
						"Value must implement Comparable<T> interface.", value);
		}
	}

	public Object getDefaultValue() {
		return this.defaultValue;
	}

	public Class<?> getType() {
		return this.type;
	}

	public Class<?> getComponenType() {
		return this.componentType;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj != null && obj.getClass().equals(this.getClass())) {
			Option op = (Option) obj;
			boolean isEqual = true;
			isEqual = this.getName().equals(op.getName()) && isEqual;

			if (this.getDescription() != null && op.getDescription() != null)
				isEqual = this.getDescription().equals(op.getDescription())
						&& isEqual;
			else
				isEqual = (this.getDescription() == op.getDescription())
						&& isEqual;

			isEqual = this.getType().equals(op.getType()) && isEqual;

			if (this.getComponenType() != null && op.getComponenType() != null)
				isEqual = this.getComponenType().equals(op.getComponenType())
						&& isEqual;
			else
				isEqual = (this.getComponenType() == op.getComponenType())
						&& isEqual;

			if (this.getDefaultValue() != null && op.getDefaultValue() != null)
				isEqual = this.getDefaultValue().equals(op.getDefaultValue())
						&& isEqual;
			else
				isEqual = this.getDefaultValue() == op.getDefaultValue()
						&& isEqual;
			return isEqual;
		}
		return false;
	}

	@Override
	public int hashCode() {
		return this.getName().hashCode();
	}
}
